<?php

declare(strict_types=1);

namespace xpzhu\Jwt;

use DateTimeZone;
use DateTimeImmutable;
use DateTimeInterface;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Signer\Key\InMemory;
use xpzhu\Jwt\Handle\RequestToken;

class Jwt
{
    /**
     * @var JwtAuth
     */
    protected JwtAuth $auth;

    /**
     * @var Config
     */
    protected Config $config;

    /**
     * @var array
     */
    protected array $claims;

    /**
     * @var Token
     */
    protected Token $token;

    /**
     * @var Configuration
     */
    private Configuration $jwtConfiguration;

    /**
     * Jwt constructor.
     * @param JwtAuth $jwt
     * @param Config  $config
     */
    public function __construct(JwtAuth $jwt, Config $config)
    {
        $this->auth   = $jwt;
        $this->config = $config;

        $this->initJwtConfiguration();
    }

    public function initJwtConfiguration()
    {
        $this->jwtConfiguration = Configuration::forSymmetricSigner(
            $this->config->getSigner(),
            $this->config->getSignerKey()
        );
    }

    public function getStore()
    {
        return $this->auth->getStore();
    }

    /**
     * 验证成功的 Token
     * @return Token
     */
    public function getToken()
    {
        return $this->token;
    }

    /**
     * 生成 Token
     * @param mixed $identifier
     * @param array $claims
     * @return Token
     */
    public function make($identifier, array $claims = []): Token
    {
        $now     = new DateTimeImmutable();
        $builder = $this->jwtConfiguration->builder()
            ->permittedFor($this->config->getAud())
            ->issuedBy($this->config->getIss())
            ->identifiedBy((string)$identifier)
            ->issuedAt($now)
            ->canOnlyBeUsedAfter($now)
            ->expiresAt($this->getExpiryDateTime($now))
            ->relatedTo((string)$identifier)
            ->withClaim('store', $this->getStore());

        foreach ($claims as $key => $value) {
            $builder->withClaim($key, $value);
        }

        return $builder->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
    }

    public function getExpiryDateTime($now): DateTimeImmutable
    {
        $ttl = (string)$this->config->getExpires();
        return $now->modify("+{$ttl} sec");
    }

    /**
     * 解析 Token
     * @param string $token
     * @return Token
     */
    public function parse(string $token): Token
    {
        $this->token = $this->jwtConfiguration->parser()->parse($token);
        return $this->token;
    }

    protected function getValidateConfig()
    {
        return Configuration::forSymmetricSigner(
            $this->config->getSigner(),
            $this->config->RSASigner() ? $this->config->getPublicKey() : $this->config->getHmacKey()
        );
    }

    /**
     * 效验合法性 Token
     * @param string $token
     * @return bool
     */
    public function validate(string $token): bool
    {
        $token = $this->parse($token);

        $jwtConfiguration = $this->getValidateConfig();

        $jwtConfiguration->setValidationConstraints(
            new SignedWith($jwtConfiguration->signer(), $jwtConfiguration->signingKey())
        );

        $constraints = $jwtConfiguration->validationConstraints();

        return $jwtConfiguration->validator()->validate($token, ...$constraints);
    }

    /**
     * 效验是否过期 Token
     * @param string $token
     * @return bool
     */
    public function validateExp(string $token): bool
    {
        $token = $this->parse($token);

        $jwtConfiguration = $this->getValidateConfig();

        $jwtConfiguration->setValidationConstraints(
            new ValidAt(new SystemClock(new DateTimeZone(\date_default_timezone_get()))),
        );

        $constraints = $jwtConfiguration->validationConstraints();

        return $jwtConfiguration->validator()->validate($token, ...$constraints);
    }

    /**
     * 获取 Token
     * @return string
     */
    public function getRequestToken(): string
    {
        $requestToken = new RequestToken();
        return $requestToken->get($this->config->getType());
    }

    public function isRefreshExpired(DateTimeInterface $now): bool
    {
        if (!$this->token->claims()->has('iat')) {
            return false;
        }

        $iat         = $this->token->claims()->get('iat');
        $refresh_ttl = $this->config->getRefreshTtl();
        $refresh_exp = $iat->modify("+{$refresh_ttl} sec");
        return $now >= $refresh_exp;
    }

    /**
     * @return Config
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Token 自动续期
     * @param Token $token
     * @return Token
     */
    public function automaticRenewalToken(Token $token)
    {
        $claims = $token->claims()->all();

        $jti = $claims['jti'];
        unset($claims['aud']);
        unset($claims['iss']);
        unset($claims['jti']);
        unset($claims['iat']);
        unset($claims['nbf']);
        unset($claims['exp']);
        unset($claims['sub']);

        $token     = $this->make($jti, $claims);
        $refreshAt = $this->config->getRefreshTtl();

        header('Access-Control-Expose-Headers:Automatic-Renewal-Token,Automatic-Renewal-Token-RefreshAt');
        header('Automatic-Renewal-Token:' . $token->toString());
        header('Automatic-Renewal-Token-RefreshAt:' . $refreshAt);

        return $token;
    }

    public function getClaims()
    {
        return $this->token->claims()->all();
    }

    public function getClaim($name)
    {
        return $this->token->claims()->get($name);
    }
}
